Odemkněte pokročilé zpracování videa v prohlížeči. Naučte se přímému přístupu a manipulaci se surovými daty rovin VideoFrame pomocí WebCodecs API pro vlastní efekty a analýzu.
WebCodecs VideoFrame Plane Access: Hloubkový pohled na manipulaci se surovými video daty
Po léta se vysoce výkonné zpracování videa ve webovém prohlížeči zdálo jako vzdálený sen. Vývojáři byli často omezeni limity elementu <video> a 2D Canvas API, které, ačkoliv byly výkonné, přinášely úzká hrdla ve výkonu a omezený přístup k podkladovým surovým video datům. Příchod WebCodecs API zásadně změnil tuto situaci a poskytl nízkoúrovňový přístup k vestavěným mediálním kodekům prohlížeče. Jednou z jeho nejrevolučnějších vlastností je schopnost přímo přistupovat a manipulovat se surovými daty jednotlivých video snímků prostřednictvím objektu VideoFrame.
Tento článek je komplexním průvodcem pro vývojáře, kteří se chtějí posunout za hranice jednoduchého přehrávání videa. Prozkoumáme složitosti přístupu k rovinám VideoFrame, demystifikujeme koncepty jako barevné prostory a rozložení v paměti a poskytneme praktické příklady, které vám umožní vytvářet novou generaci video aplikací v prohlížeči, od filtrů v reálném čase po sofistikované úlohy počítačového vidění.
Předpoklady
Abyste z tohoto průvodce vytěžili co nejvíce, měli byste mít solidní znalosti:
- Moderní JavaScript: Včetně asynchronního programování (
async/await, Promises). - Základní koncepty videa: Znalost pojmů jako snímky, rozlišení a kodeky je užitečná.
- API prohlížeče: Zkušenosti s API jako Canvas 2D nebo WebGL budou výhodou, ale nejsou striktně vyžadovány.
Porozumění video snímkům, barevným prostorům a rovinám
Než se ponoříme do API, musíme si nejprve vytvořit solidní mentální model toho, jak data video snímku ve skutečnosti vypadají. Digitální video je sekvence statických obrázků neboli snímků. Každý snímek je mřížka pixelů a každý pixel má barvu. Jak je tato barva uložena, je definováno barevným prostorem a formátem pixelů.
RGBA: Rodný jazyk webu
Většina webových vývojářů je obeznámena s barevným modelem RGBA. Každý pixel je reprezentován čtyřmi složkami: červenou (Red), zelenou (Green), modrou (Blue) a alfa (průhlednost). Data jsou typicky uložena prokládaně (interleaved) v paměti, což znamená, že hodnoty R, G, B a A pro jeden pixel jsou uloženy za sebou:
[R1, G1, B1, A1, R2, G2, B2, A2, ...]
V tomto modelu je celý obrázek uložen v jediném souvislém bloku paměti. Můžeme si to představit jako jednu „rovinu“ dat.
YUV: Jazyk video komprese
Video kodeky však zřídka pracují přímo s RGBA. Preferují barevné prostory YUV (nebo přesněji Y'CbCr). Tento model odděluje obrazové informace na:
- Y (Luma): Jasová neboli černobílá informace. Lidské oko je nejcitlivější na změny jasu.
- U (Cb) a V (Cr): Chrominanční neboli informaci o barevném rozdílu. Lidské oko je méně citlivé na barevné detaily než na detaily jasu.
Toto oddělení je klíčové pro efektivní kompresi. Snížením rozlišení složek U a V – technika zvaná podvzorkování barvonosné složky (chroma subsampling) – můžeme výrazně zmenšit velikost souboru s minimální vnímatelnou ztrátou kvality. To vede k planárním formátům pixelů, kde jsou složky Y, U a V uloženy v oddělených blocích paměti neboli „rovinách“.
Běžným formátem je I420 (typ YUV 4:2:0), kde na každý blok 2x2 pixelů připadají čtyři vzorky Y, ale pouze jeden vzorek U a jeden V. To znamená, že roviny U a V mají poloviční šířku a poloviční výšku než rovina Y.
Pochopení tohoto rozdílu je zásadní, protože WebCodecs vám dává přímý přístup právě k těmto rovinám, přesně tak, jak je poskytuje dekodér.
Objekt VideoFrame: Vaše brána k pixelovým datům
Ústředním prvkem této skládačky je objekt VideoFrame. Reprezentuje jediný snímek videa a obsahuje nejen pixelová data, ale také důležitá metadata.
Klíčové vlastnosti VideoFrame
format: Řetězec udávající formát pixelů (např. 'I420', 'NV12', 'RGBA').codedWidth/codedHeight: Plné rozměry snímku, jak jsou uloženy v paměti, včetně jakéhokoli zarovnání (padding) vyžadovaného kodekem.displayWidth/displayHeight: Rozměry, které by se měly použít pro zobrazení snímku.timestamp: Prezentacní časové razítko snímku v mikrosekundách.duration: Doba trvání snímku v mikrosekundách.
Magická metoda: copyTo()
Hlavní metodou pro přístup k surovým pixelovým datům je videoFrame.copyTo(destination, options). Tato asynchronní metoda kopíruje data rovin snímku do vámi poskytnutého bufferu.
destination:ArrayBuffernebo typované pole (jakoUint8Array) dostatečně velké na to, aby pojalo data.options: Objekt, který specifikuje, které roviny se mají kopírovat a jaké je jejich rozložení v paměti. Pokud je vynechán, zkopíruje všechny roviny do jediného souvislého bufferu.
Metoda vrací Promise, který se resolvuje s polem objektů PlaneLayout, jeden pro každou rovinu ve snímku. Každý objekt PlaneLayout obsahuje dvě klíčové informace:
offset: Odsazení v bajtech, kde data této roviny začínají v cílovém bufferu.stride: Počet bajtů mezi začátkem jednoho řádku pixelů a začátkem dalšího řádku pro danou rovinu.
Zásadní koncept: Stride vs. šířka
Toto je jeden z nejčastějších zdrojů zmatků pro vývojáře, kteří jsou v nízkoúrovňovém grafickém programování noví. Nemůžete předpokládat, že každý řádek pixelových dat je těsně za sebou.
- Šířka (Width) je počet pixelů v řádku obrázku.
- Stride (také nazývaný pitch nebo line step) je počet bajtů v paměti od začátku jednoho řádku k začátku dalšího.
Často bude stride větší než šířka * bajtů_na_pixel. Důvodem je, že paměť je často zarovnána (padded), aby odpovídala hardwarovým hranicím (např. 32- nebo 64-bajtovým hranicím) pro rychlejší zpracování CPU nebo GPU. Pro výpočet paměťové adresy pixelu v konkrétním řádku musíte vždy použít stride.
Ignorování stride povede ke zkresleným nebo deformovaným obrázkům a nesprávnému přístupu k datům.
Praktický příklad 1: Přístup a zobrazení roviny ve stupních šedi
Začněme jednoduchým, ale silným příkladem. Většina videa na webu je kódována ve formátu YUV, jako je I420. Rovina 'Y' je v podstatě kompletní reprezentace obrázku ve stupních šedi. Můžeme extrahovat pouze tuto rovinu a vykreslit ji na canvas.
async function displayGrayscale(videoFrame) {
// Předpokládáme, že videoFrame je ve formátu YUV, jako 'I420' nebo 'NV12'.
if (!videoFrame.format.startsWith('I4')) {
console.error('Tento příklad vyžaduje planární formát YUV 4:2:0.');
videoFrame.close();
return;
}
const yPlaneInfo = videoFrame.layout[0]; // Rovina Y je vždy první.
// Vytvoříme buffer pro uložení dat pouze z roviny Y.
const yPlaneData = new Uint8Array(yPlaneInfo.stride * videoFrame.codedHeight);
// Zkopírujeme rovinu Y do našeho bufferu.
await videoFrame.copyTo(yPlaneData, {
rect: { x: 0, y: 0, width: videoFrame.codedWidth, height: videoFrame.codedHeight },
layout: [yPlaneInfo]
});
// Nyní yPlaneData obsahuje surové pixely ve stupních šedi.
// Musíme to vykreslit. Vytvoříme RGBA buffer pro canvas.
const canvas = document.getElementById('my-canvas');
canvas.width = videoFrame.displayWidth;
canvas.height = videoFrame.displayHeight;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(canvas.width, canvas.height);
// Projdeme pixely canvasu a naplníme je daty z roviny Y.
for (let y = 0; y < videoFrame.displayHeight; y++) {
for (let x = 0; x < videoFrame.displayWidth; x++) {
// Důležité: Použijte stride k nalezení správného zdrojového indexu!
const yIndex = y * yPlaneInfo.stride + x;
const luma = yPlaneData[yIndex];
// Vypočítáme cílový index v RGBA bufferu ImageData.
const rgbaIndex = (y * canvas.width + x) * 4;
imageData.data[rgbaIndex] = luma; // Červená
imageData.data[rgbaIndex + 1] = luma; // Zelená
imageData.data[rgbaIndex + 2] = luma; // Modrá
imageData.data[rgbaIndex + 3] = 255; // Alfa
}
}
ctx.putImageData(imageData, 0, 0);
// KRITICKÉ: Vždy zavolejte close() na VideoFrame, abyste uvolnili jeho paměť.
videoFrame.close();
}
Tento příklad zdůrazňuje několik klíčových kroků: identifikaci správného rozložení roviny, alokaci cílového bufferu, použití copyTo k extrakci dat a správné procházení dat pomocí stride k sestavení nového obrázku.
Praktický příklad 2: Manipulace na místě (Sépiový filtr)
Nyní provedeme přímou manipulaci s daty. Sépiový filtr je klasický efekt, který je snadné implementovat. Pro tento příklad je jednodušší pracovat se snímkem RGBA, který můžete získat z canvasu nebo z WebGL kontextu.
async function applySepiaFilter(videoFrame) {
// Tento příklad předpokládá, že vstupní snímek je 'RGBA' nebo 'BGRA'.
if (videoFrame.format !== 'RGBA' && videoFrame.format !== 'BGRA') {
console.error('Příklad se sépiovým filtrem vyžaduje RGBA snímek.');
videoFrame.close();
return null;
}
// Alokujeme buffer pro uložení pixelových dat.
const frameDataSize = videoFrame.allocationSize();
const frameData = new Uint8Array(frameDataSize);
await videoFrame.copyTo(frameData);
const layout = videoFrame.layout[0]; // RGBA je jediná rovina
// Nyní manipulujeme s daty v bufferu.
for (let y = 0; y < videoFrame.codedHeight; y++) {
for (let x = 0; x < videoFrame.codedWidth; x++) {
const pixelIndex = y * layout.stride + x * 4; // 4 bajty na pixel (R,G,B,A)
const r = frameData[pixelIndex];
const g = frameData[pixelIndex + 1];
const b = frameData[pixelIndex + 2];
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
frameData[pixelIndex] = Math.min(255, tr);
frameData[pixelIndex + 1] = Math.min(255, tg);
frameData[pixelIndex + 2] = Math.min(255, tb);
// Alfa (frameData[pixelIndex + 3]) zůstává nezměněna.
}
}
// Vytvoříme *nový* VideoFrame s upravenými daty.
const newFrame = new VideoFrame(frameData, {
format: videoFrame.format,
codedWidth: videoFrame.codedWidth,
codedHeight: videoFrame.codedHeight,
timestamp: videoFrame.timestamp,
duration: videoFrame.duration
});
// Nezapomeňte zavřít původní snímek!
videoFrame.close();
return newFrame;
}
Toto demonstruje kompletní cyklus čtení-úprava-zápis: zkopírování dat, jejich procházení ve smyčce s použitím stride, aplikace matematické transformace na každý pixel a vytvoření nového VideoFrame s výslednými daty. Tento nový snímek pak může být vykreslen na canvas, odeslán do VideoEncoder nebo předán dalšímu kroku zpracování.
Na výkonu záleží: JavaScript vs. WebAssembly (WASM)
Procházení milionů pixelů pro každý snímek (snímek 1080p má přes 2 miliony pixelů, neboli 8 milionů datových bodů v RGBA) v JavaScriptu může být pomalé. Ačkoliv moderní JS enginy jsou neuvěřitelně rychlé, pro zpracování videa s vysokým rozlišením (HD, 4K) v reálném čase může tento přístup snadno přetížit hlavní vlákno, což vede k trhanému uživatelskému zážitku.
Zde se stává WebAssembly (WASM) nezbytným nástrojem. WASM vám umožňuje spouštět kód napsaný v jazycích jako C++, Rust nebo Go téměř nativní rychlostí přímo v prohlížeči. Pracovní postup pro zpracování videa se stává:
- V JavaScriptu: Použijte
videoFrame.copyTo()k získání surových pixelových dat doArrayBuffer. - Předání do WASM: Předejte referenci na tento buffer do vašeho zkompilovaného WASM modulu. Jedná se o velmi rychlou operaci, protože nezahrnuje kopírování dat.
- Ve WASM (C++/Rust): Spusťte své vysoce optimalizované algoritmy pro zpracování obrazu přímo na paměťovém bufferu. To je o řády rychlejší než smyčka v JavaScriptu.
- Návrat do JavaScriptu: Jakmile WASM skončí, řízení se vrací do JavaScriptu. Poté můžete použít upravený buffer k vytvoření nového
VideoFrame.
Pro jakoukoli seriózní aplikaci pro manipulaci s videem v reálném čase – jako jsou virtuální pozadí, detekce objektů nebo složité filtry – není využití WebAssembly jen možností, ale nutností.
Zpracování různých formátů pixelů (např. I420, NV12)
Ačkoliv je RGBA jednoduché, nejčastěji budete od VideoDecoder dostávat snímky v planárních formátech YUV. Podívejme se, jak zpracovat plně planární formát jako I420.
VideoFrame ve formátu I420 bude mít ve svém poli layout tři deskriptory rozložení:
layout[0]: Rovina Y (luma). Rozměry jsoucodedWidthxcodedHeight.layout[1]: Rovina U (chroma). Rozměry jsoucodedWidth/2xcodedHeight/2.layout[2]: Rovina V (chroma). Rozměry jsoucodedWidth/2xcodedHeight/2.
Takto byste zkopírovali všechny tři roviny do jediného bufferu:
async function extractI420Planes(videoFrame) {
const totalSize = videoFrame.allocationSize({ format: 'I420' });
const allPlanesData = new Uint8Array(totalSize);
const layouts = await videoFrame.copyTo(allPlanesData);
// layouts je pole 3 objektů PlaneLayout
console.log('Y Plane Layout:', layouts[0]); // { offset: 0, stride: ... }
console.log('U Plane Layout:', layouts[1]); // { offset: ..., stride: ... }
console.log('V Plane Layout:', layouts[2]); // { offset: ..., stride: ... }
// Nyní můžete přistupovat ke každé rovině v bufferu `allPlanesData`
// pomocí jejího specifického offsetu a stride.
const yPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[0].offset,
layouts[0].stride * videoFrame.codedHeight
);
// Všimněte si, že rozměry chroma jsou poloviční!
const uPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[1].offset,
layouts[1].stride * (videoFrame.codedHeight / 2)
);
const vPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[2].offset,
layouts[2].stride * (videoFrame.codedHeight / 2)
);
console.log('Accessed Y plane size:', yPlaneView.byteLength);
console.log('Accessed U plane size:', uPlaneView.byteLength);
videoFrame.close();
}
Dalším běžným formátem je NV12, který je semi-planární. Má dvě roviny: jednu pro Y a druhou rovinu, kde jsou hodnoty U a V prokládány (např. [U1, V1, U2, V2, ...]). WebCodecs API toto řeší transparentně; VideoFrame ve formátu NV12 bude mít ve svém poli layout jednoduše dvě rozložení.
Výzvy a osvědčené postupy
Práce na takto nízké úrovni je mocná, ale přináší s sebou odpovědnost.
Správa paměti je prvořadá
VideoFrame drží značné množství paměti, která je často spravována mimo haldu JavaScriptového garbage collectoru. Pokud tuto paměť explicitně neuvolníte, způsobíte únik paměti, který může vést k pádu záložky prohlížeče.
Vždy, vždy volejte videoFrame.close(), když jste se snímkem hotovi.
Asynchronní povaha
Veškerý přístup k datům je asynchronní. Architektura vaší aplikace musí správně zpracovávat tok Promises a async/await, aby se předešlo závodním podmínkám (race conditions) a zajistil se hladký průběh zpracování.
Kompatibilita s prohlížeči
WebCodecs je moderní API. Ačkoliv je podporováno ve všech hlavních prohlížečích, vždy zkontrolujte jeho dostupnost a buďte si vědomi jakýchkoli specifických implementačních detailů nebo omezení jednotlivých výrobců. Před pokusem o použití API použijte detekci funkcí.
Závěr: Nová hranice pro webové video
Schopnost přímo přistupovat a manipulovat se surovými daty rovin VideoFrame prostřednictvím WebCodecs API je paradigmatickou změnou pro webové mediální aplikace. Odstraňuje černou skříňku elementu <video> a dává vývojářům granulární kontrolu, která byla dříve vyhrazena nativním aplikacím.
Porozuměním základům rozložení video paměti – rovinám, stride a barevným formátům – a využitím síly WebAssembly pro výkonově kritické operace můžete nyní vytvářet neuvěřitelně sofistikované nástroje pro zpracování videa přímo v prohlížeči. Od barevných korekcí v reálném čase a vlastních vizuálních efektů po strojové učení na straně klienta a analýzu videa, možnosti jsou obrovské. Éra vysoce výkonného, nízkoúrovňového videa na webu skutečně začala.